#-------------------------------------------------------------------------------
# Name:        network_scan
# Purpose:     WMI object scanner for Windows networks.
#
# Version:     20130117.2
#
#-------------------------------------------------------------------------------

# -*- coding: utf-8 -*-

import re
import sys
import os
import string
import csv
import wmi
import datetime
import xlwt
import logging
import logging.config
import sqlite3 as lite
from xlwt import Workbook
from subprocess import Popen


logging.config.fileConfig('log_config.txt')
logConn = logging.getLogger('connect')
logEvents = logging.getLogger('events')
logDefault = logging.getLogger('manager')
logDefault.info('###                 Program started                 ###')

class Dirs(object):
    rootDir = os.getcwd()
    scanDir = os.path.join(rootDir, 'Scan Data')
    summaryDir = os.path.join(scanDir, 'Summary')
    allHostsDir = os.path.join(scanDir, 'Hosts')

class SqlDB(object):

    def __init__(self, dbName):
        self.dbPath = os.path.join(os.curdir,dbName)
        self.eventTable = []
        self.summaryTable = []

    def Connect(self):
        while True:
            if os.path.exists(self.dbPath):
                if responses.ask('Database already exists.  Connect? (y/n)', 'binary'):
                    break
                else:
                    answer = responses.ask('Enter full name for database.', 'text')
                    self.dbPath = os.path.join(os.curdir, answer)
            else:
                break
        try:
            self.conn = lite.connect(self.dbPath)
        except:
            print 'Connection failed.'
            raise

        else:
            self.dbActive = self.conn.cursor()

    def AddTable(self, tableName, opts):
        self.dbActive.execute('CREATE TABLE IF NOT EXISTS ' + tableName + opts)

    def InsertAll(self, tableName, data, pattern):
        try:
            self.dbActive.executemany('INSERT INTO ' + tableName + ' VALUES ' \
                                      + pattern, data)
        except:
            print [data]
            raise
    def Insert(self, tableName, data):
        self.dbActive.execute("INSERT INTO " + tableName + " VALUES " + str(data))

    def Delete(self, tableName, param, match):
        self.dbActive.execute("DELETE FROM " + tableName + " WHERE " + param + "=" + match)

    def Commit(self):
        self.conn.commit()
        self.conn.close()

    def WriteSummary(self):
        Var.database.InsertAll('summary', self.summaryTable, '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')

    def WriteEvents(self):
        Var.database.InsertAll('events', self.eventTable, '(?, ?, ?, ?, ?, ?, ?)')

    def addToSummary(self, data):
        self.summaryTable.append(data)

    def addToEvents(self, host, data):
        for e in data[2:]:
            self.eventTable.append([host] + e[:-1])



class GetNet(object):

    def __init__(self):
        self.hosts = []

    def options(self):
        c = responses.ask('''Please choose from one of the following options:\n
        \t1. Load targets from CSV.
        \t2. Scan network with NMAP.
        \t3. Enter single target name or IP.
        \t4. Build hosts list.\n''', 'multi', ['1', '2', '3', '4'])
        return ([responses.ask('Enter IP address or hostname to scan.', 'text')] if c == '3' else
                self.hostfile() if c == '1' else
                self.netscan() if c == '2' else
                self.build_hosts() if c == '4' else
                get_hosts()
                )

    def hostfile(self, filename=None):
        """ Parse host file for targets. """
        if filename is None:
            filename = responses.ask('Enter filename:', 'file')
        print ''
        print 'Parsing {} now.'.format(str(filename).upper())
        with open(filename, 'rb') as f:
            parse = csv.reader(f)
            for line in parse:
                try:
                    self.hosts.append(line[0])
                except IndexError:
                    break
        print 'Complete.'
        print ''
        logDefault.info('Retrieved hosts from host file')
        return self.checkHosts()

    def netscan(self):
        """ Scan the network for targets using nmap.
            If multiple interfaces exist on the local machine you choose which to use. """
        import socket
        host = socket.gethostname()
        ips = [ip for ip in socket.gethostbyname_ex(host)[2]
               if not ip.startswith('127.')]
        networks = {str(i + 1):'%s.%s.%s.0/24' % re.search(r'(\d+)\.(\d+)\.(\d+)\.\d+', ips[i]).groups()
                    for i in range(len(ips))}
        if len(networks) > 1:
            print 'The following networks were found:'
            print '\n'.join(['%s. %s' % (k,v) for k,v in networks.items()])
            choice = responses.ask('Please choose a network', 'multi', [str(i) for i in range(len(networks.items()))])
            if choice in networks.keys():
                netscan = networks[choice]
            elif choice == '':
                netscan = networks['1']
            else:
                print 'Choice out of range.'
                sys.exit()
        elif len(networks) == 1:
            netscan = networks['1']
            print '%s network found, setting as scan target.\n' % netscan
        else:
            print 'No networks found.  Quitting.'
            sys.exit()
        try:
            import nmap
            nm = nmap.PortScanner()
        except WindowsError:
            print 'Unable to initialize nmap.  Please check installation.'
            sys.exit()
        nscan = nm.scan(hosts=netscan, arguments='-sS -p 135')
        self.hosts = [(nscan['scan'][s]['hostname'], s) for s in nscan['scan']
                      if 'open' in nscan['scan'][s]['tcp'][135]['state']]
        Var.fm.CSVComplete('acquired_hosts.csv', self.hosts)
        logDefault.info('Retrieved hosts from nmap scan')
        return self.checkHosts

    def build_hosts(self):
        """ Create a target file by entering ip/hostname pairs. """
        saveit = responses.ask('Save to disk? (y/n)', 'binary')
        while True:
            hname = responses.ask('Enter host name, leave blank and press Enter if done.', 'text')
            if hname == '':
                break
            else:
                self.hosts.append(hname)
            print hname
        print ''
        print self.hosts
        print 'Host list built.'
        if saveit is True:
            Var.fm.CSVComplete('build_hosts.csv', [[e] for e in self.hosts])
            logDefault.info('Hosts list built')
        return self.checkHosts()

    def get_ip(self, host):
        return socket.gethostbyname(host)

    def checkHosts(self):
        if len(set(self.hosts)) < len(self.hosts):
            print ''
            print 'Duplicate hosts found; Removing.'
            return list(set(self.hosts))
        return self.hosts


class FileManager(object):

    def __init__(self):
        self.book = Workbook(encoding='utf-8')
        self.clist = []
        self.style = xlwt.easyxf('pattern: pattern solid, fore_colour ice_blue')

    def CSVWriter(self, fn):
        self.file = open(fn, 'wb')
        self.csvw = csv.writer(self.file)

    def CSVWriteNull(self, fn):
        with open(fn, 'wb') as f:
            _csv = csv.writer(f)
            _csv.writerow(['No Records'])

    def CSVComplete(self, fn, data, hostDir):
        self.file = open(os.path.join(hostDir, fn), 'wb')
        self.csvw = csv.writer(self.file)
        try:
            self.csvw.writerows(data)
        except:
            for i in data:
                print i
            raise
        self.clist.append(self.file)
        self.file.close()

    def CSVReader(self, fn):
        self.file = open(fn, 'rb')
        self.csvr = csv.reader(self.file)

    def CSVAddAll(self, data):
        self.csvw.writerows(data)

    def CSVAddLine(self, data):
        self.csvw.writerow(data)

    def CSVClose(self):
        self.clist.append(self.file)
        self.file.close()

    def XLSAddSheet(self, sheet):
        self.sum_sheet = self.book.add_sheet(sheet)

    def XLSsWrite(self, sheet, data):
        sheet_reference = ''
        add_link = False
        do_links = True
        for row_index, row_contents in enumerate(data):
            for column_index, cell_value in enumerate(row_contents):
                if column_index == 0:
                    if cell_value == 'Summary' or cell_value == 'Free Disk':
                        do_links, add_link = True, False
                        sheet_reference = ''
                    elif cell_value == '':
                        do_links, add_link = False, False
                        sheet_reference = ''
                    elif cell_value == 'Event Count':
                        do_links, add_link = True, False
                        sheet_reference = ' Events'
                    else:
                        add_link = True
                if column_index == 0:
                    if cell_value != 'Host' and cell_value != '' and do_links is True and add_link is True:
                        short = makeShort(cell_value)
                        self.sum_sheet.write(row_index, column_index,
                        xlwt.Formula('HYPERLINK("#\'{0}{1}\'!A1", "{2}")'.format(short, sheet_reference, cell_value)), self.style)
                    else:
                        self.sum_sheet.write(row_index, column_index, cell_value)
                else:
                    self.sum_sheet.write(row_index, column_index, cell_value)

    def XLSWrite(self, shortHost, data):
        sheet = self.book.add_sheet(shortHost)
        if data is None:
            sheet.write(0, 0, 'No Records')
        else:
            data.insert(0, [''])
            for row_index, row_contents in enumerate(data):
                for column_index, cell_value in enumerate(row_contents):
                    if row_index == column_index == 0:
                        sheet.write(0, 0, xlwt.Formula('HYPERLINK("#\'Scan Summary\'!A1", "<------")'), self.style)
                    else:
                        sheet.write(row_index, column_index, cell_value)

    def XLSSave(self, fn):
        with open(fn, 'wb'):
            self.book.save(fn)


class GetInput(object):

    def __init__(self):
        self.qdict = {}
        self.type_dict = {'binary': self._binary,
                          'file': self._fname,
                          'multi': self._multi,
                          'text': self._text,
                          'num': self._number
                          }

    def add(self, question, answer):
        self.qdict[question] = answer

    def ask(self, question, atype, akey=None):
        self.question = question
        self.atype = atype
        self.akey = akey
        try:
            print ''
            answer = raw_input('{0} > '.format(question))
        except KeyboardInterrupt:
            print 'Input not recognized. Input valid response or type \'q\' to quit.'
            self.ask(self.question, self.atype, self.akey)
        else:
            if answer == 'q': sys.exit()
            if self.akey and answer not in self.akey:
                print 'Response not in accepted answer list.'
                self.ask(self.question, self.type, self.akey)
            self.qdict[question] = answer
            return (self.type_dict[atype](answer, akey))

    def _multi(self, answer, akey):
        """ Ask the user a multiple choice question. """
        if answer.lower() not in akey:
            print 'Response is not acceptable.'
            print ''
            return self.ask(self.question, 'multi', akey)
        return answer


    def _binary(self, answer, akey=None):
        """ Ask the user a question with a yes or no response.
            Store the question and answer in self.qdict. """
        pos = ['Y', 'YES', 'TRUE']
        neg = ['N', 'NO', 'FALSE']
        if answer.upper() in pos: return True
        elif answer.upper() in neg: return False
        else:
            print 'Response not in accepted answer list.'
            print ''
            return self.ask(self.question, 'binary')

    def _fname(self, answer, akey=None):
        """ Ask the user a question where the answer is a filename.
            Verify the filename exists.
            Store the question and answer in self.qdict. """
        if answer == '':
            print 'No file specified.'
            return ''
        elif '.csv' not in answer:
            print 'Only CSV files are accepted.'
            print ''
            return self.ask(self.question, 'file')
        elif not os.path.exists(answer):
            print 'File not found.'
            print ''
            return self.ask(self.question, 'file')
        else: return answer

    def _text(self, answer, akey=None):
        """ Return raw text. """
        return answer

    def _number(self, answer, akey=None):
        """ Return a number. """
        try:
            return int(answer)
        except:
            raise ValueError('Response is not an integer.')



class Configure(object):

    def __init__(self):
        self.config= {'EventHistory': ['not configured', 'num'],
                      'HostFile': ['not configured', 'file']
                     }
        self.DoConfig()

    def _setAttr(self):
        print ''
        for key, value in self.config.items():
            print '{0} > {1}'.format(key, value)
            setattr(self, key, value)
        print ''

    def _parseConfig(self):
        try:
            f = open('scan_config.txt', 'rU')
        except:
            logDefault.warning('Could not process configuration')
        else:
            logDefault.info('Configuration processed')
            if responses.ask('Load configuration? (y/n)', 'binary'):
                for line in f.readlines():
                    key, value = line.split('=')
                    self.config[key.strip()] = value.strip()

    def DoConfig(self):
        self._parseConfig()
        for option in self.config:
            if isinstance(self.config[option], list):
                self.config[option] = responses.ask(option, self.config[option][1])
        self._setAttr()


# A few necessary globals
responses = GetInput()
config = Configure()





def format_date(date):
    tsearch = re.compile(r'^(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})(?P<hour>\d{2})(?P<minute>\d{2})(?P<second>\d{2})')
    try:
        t = re.search(tsearch, date)
    except TypeError:
        t = ''
    try:
        time = '{0}/{1}/{2} {3}:{4}:{5}'.format(t.group('month'), t.group('day'), t.group('year'),
                                                t.group('hour'), t.group('minute'), t.group('second'))
    except:
        return ''
    else:
        return time

def dates():
    logDefault.info('Getting date information')
    nowDate = datetime.datetime.now()
    lastMonth = nowDate - datetime.timedelta(days=28)
    today = [nowDate.year, nowDate.month, nowDate.day, '{0}:{1}:{2}'.format(nowDate.hour, nowDate.minute, nowDate.second)]
    scandate = '{1}/{2}/{0} {3}'.format(today[0], str(today[1]).zfill(2), str(today[2]).zfill(2), today[3])
    transtable = string.maketrans('/:', "-'")
    fdate = scandate.translate(transtable)
    zipdate = '{0} - {1}'.format(lastMonth.year, str(lastMonth.month).zfill(2))
    if config.EventHistory == 0:
        fromdate = 'all'
    elif config.EventHistory == -1:
        fromdate = None
    else:
        oldDate = nowDate - datetime.timedelta(days=config.EventHistory)
        targetdate = [oldDate.year, oldDate.month, oldDate.day, '{0}:{1}:{2}'.format(oldDate.hour, oldDate.minute, oldDate.second)]
        fromdate = '{1}/{2}/{0} 00:00:00'.format(targetdate[0], targetdate[1], targetdate[2])
    return today, fromdate, scandate, fdate, zipdate

def credentials():
    import getpass
    logDefault.info('Getting credentials')
    uname = raw_input('Enter username: ')
    pword = getpass.win_getpass()
    if uname == '' and pword == '':
        logDefault.warning('No credentials supplied')
    return uname, pword



class Var(object):
    uname, pword = credentials()
    fm = FileManager()
    today, fromdate, scandate, fdate, zipdate = dates()
    nowDate = datetime.datetime.now()
    homeDir = ''
    bad_hosts = []
    disk_alert = []
    event_count = []
    sum_header = ['Host', 'Scan Date',
                  'Manufacturer', 'Serial Number', 'Description', 'Version', 'Release Date',
                  'Name',
                  'Manufacturer', 'Model', 'Logical Procs', 'Memory', 'Username',
                  'Name', 'Driver', 'Driver Date',
                  'Caption', 'SP Major', 'SP Minor', 'Architecture', 'Install Date']
    sum_table = """(Host TEXT, ScanDate TEXT, BIOS TEXT, Serial TEXT,
                    Description TEXT, Version TEXT, RelDate TEXT, Processor TEXT,
                    SysMfr TEXT, SysModel TEXT, CPUCoreCount TEXT, Memory TEXT,
                    Username TEXT, Video TEXT, VDriver TEXT, VDriverDate TEXT,
                    OS TEXT, SPMajor INT, SPMinor INT, OSArchitecture TEXT,
                    Install TEXT, PRIMARY KEY (Host) ON CONFLICT REPLACE)"""
    event_table = """(Host TEXT, LogFile TEXT, Record INT, EventID INT, Source TEXT,
                      Message TEXT, Time TEXT, PRIMARY KEY (Host, Record, Time)
                      ON CONFLICT IGNORE)"""
    sum_data = []
    csv_space = [[''], ['']]
    database = SqlDB(os.path.join(Dirs.scanDir, 'scan.db'))
    printerDict = { 0: 'On Line',
                    1: 'Paused',
                    2: 'Pending Deletion',
                    3: 'Error',
                    4: 'Paper Jam',
                    5: 'Paper Out',
                    6: 'Manual Feed',
                    7: 'Paper Problem',
                    8: 'Offline',
                    256: 'IO Active',
                    512: 'Busy',
                    1024: 'Printing',
                    2048: 'Output Bin Full',
                    4096: 'Not Available',
                    8192: 'Waiting',
                    6384: 'Processing',
                    32768: 'Initializing',
                    65536: 'Warming Up',
                    131072: 'Toner Low',
                    262144: 'No Toner',
                    524288: 'Page Punt',
                    1048576: 'User Intervention',
                    2097152: 'Out of Memory',
                    4194304: 'Door Open',
                    8388608: 'Server Unknown',
                    16777216: 'Power Save',
                    }



class wmiData(object):

    def __init__(self, host, shortHost):
        self.host = host
        self.shortHost = shortHost
        if self.host == 'localhost' or self.host == '127.0.0.1':
            self.target = wmi.WMI(host)
        else:
            self.target = wmi.WMI(host, user=Var.uname, password=Var.pword)

    def getAttributes(self, obj, attlist):
        return [ [getattr(element, attribute, '') for attribute in attlist]
                 for element in obj ]

    def getAttributes_single(self, obj, attlist):
        return [getattr(obj, attribute, '') for attribute in attlist]

    def getHostName(self):
        return self.shortHost

    def Bios(self):
        print '  BIOS'
        _bios = self.target.Win32_BIOS()[0]
        attributes = ['Manufacturer', 'SerialNumber', 'Caption', 'SMBIOSBIOSVersion']
        self.bios = self.getAttributes_single(_bios, attributes)
        return self.bios + [format_date(_bios.ReleaseDate)]

    def Processor(self):
        print '  Processor'
        self.processor = [self.target.Win32_Processor()[0].Name]
        return self.processor

    def CompSys(self):
        print '  System'
        _comp = self.target.Win32_ComputerSystem()[0]
        attributes = ['Manufacturer', 'Model', 'NumberOfLogicalProcessors',
                      'TotalPhysicalMemory', 'UserName']
        self.compsys = self.getAttributes_single(_comp, attributes)
        return self.compsys

    def Disks(self):
        print '  Disks'
        _disks = self.target.Win32_Diskdrive()
        attributes = ['Caption', 'FirmwareRevision', 'Status']
        self.disks = self.getAttributes(_disks, attributes)
        return self.disks

    def NetAdapter(self):
        print '  Network Adapter'
        _net = self.target.Win32_NetworkAdapter(PhysicalAdapter=True)
        attributes = ['Name', 'MACAddress', 'NetConnectionID', 'NetEnabled',
                      'ServiceName']
        self.netadapter = self.getAttributes(_net, attributes)
        return self.netadapter

    def FreeDisk(self):
        print '  Disk Space'
        return [(self.host, disk.Caption, round((long(disk.FreeSpace) * 1.0 / 1024**3) / \
                (long(disk.Size) * 1.0 / 1024**3) * 100, 2))
                  for disk in self.target.Win32_LogicalDisk(DriveType=3)
                  if (long(disk.FreeSpace) * 1.0 / 1024**3) / (long(disk.Size) * 1.0 / 1024**3) < 0.25]

    def Video(self):
        print '  Video Controller'
        _video = self.target.Win32_VideoController()[0]
        attributes = ['Name', 'DriverVersion']
        self.video = self.getAttributes_single(_video, attributes)
        return self.video + [format_date(_video.DriverDate)]

    def OS(self):
        print '  Operating System'
        _os = self.target.Win32_OperatingSystem()[0]
        attributes = ['Caption', 'ServicePackMajorVersion', 'ServicePackMinorVersion',
                      'OSArchitecture']
        self.os = self.getAttributes_single(_os, attributes)
        return self.os + [format_date(_os.InstallDate)]

    def Processes(self):
        print '  Running Processes'
        category = ['Running Processes']
        header = ['Name', 'PID', 'ExecutablePath', 'WorkingSetSize']
        attributes = ['Name', 'ProcessId', 'ExecutablePath', 'WorkingSetSize']
        try:
            self.processes = self.getAttributes(self.target.Win32_Process(), attributes)
        except:
            return category, None, None
        return category, header, self.processes

    def Services(self):
        print '  Services'
        category = ['Services']
        header = ['Name', 'System Name', 'Status', 'Start Mode', 'Start User',
                  'Path', 'Accepts Pause?', 'Accepts Stop?']
        attributes = ['Caption', 'Name', 'State', 'StartMode', 'StartName', 'PathName',
                      'AcceptPause', 'AcceptStop']
        try:
            self.services = self.getAttributes(self.target.Win32_Service(), attributes)
        except:
            return category, None, None
        return category, header, self.services

    def Startup(self):
        print '  Startup'
        category = ['Startup Applications']
        header = ['Location', 'Caption', 'Command']
        attributes = ['Location', 'Caption', 'Command']
        try:
            self.startup = self.getAttributes(self.target.Win32_StartupCommand(), attributes)
        except:
            return category, None, None
        return category, header, self.startup

    def Printers(self):
        self.printers = []
        print '  Printers'
        category = ['Printers']
        header = ['Name', 'ShareName', 'IP', 'Status']
        attributes = ['Name', 'ShareName', 'PortName', 'PrinterState']
        try:
            printers = self.getAttributes(self.target.Win32_Printer(), attributes)
        except:
            return category, None, None
        for printer in printers:
            printer[-1] = Var.printerDict.get(printer[-1], None)
            if printer[-1] != 'On Line':
                logDefault.warning('Host {} printer {} status {}'.format(self.getHostName(), printer[0],
                                                                         printer[-1]))
            if printer[1]:
                self.printers.append(printer)
        return category, header, self.printers if self.printers else [['None Shared']]

    def EventLog(self):
        from operator import itemgetter
        logEvents.info('Host {} retrieving event log'.format(self.getHostName()))
        self.e_sum_header = ['LogFile', 'Event ID', 'Source', 'Message', 'First', 'Last', 'Count', 'Keywords']
        self.e_summary = []
        self.e_header = ['LogFile', 'Record', 'Event ID', 'Source', 'Message', 'Time', 'Keywords']
        self.e_list = []
        _cache = {}
        print '  Event Log'
        if Var.fromdate != 'all':
            _query = ('''Select Logfile, SourceName, RecordNumber, TimeGenerated, EventCode, Message
            from Win32_NTLogEvent where (Type=\"error\" OR Type=\"critical\") AND (Logfile=\"Application\" OR \
            Logfile=\"System\") AND TimeGenerated>=\"''' + Var.fromdate + '"').encode('utf-8')
        else:
            _query = ('''Select Logfile, SourceName, RecordNumber, TimeGenerated, EventCode, Message
            from Win32_NTLogEvent where (Type=\"error\" or Type=\"critical\") AND (Logfile=\"Application\" OR \
            Logfile=\"System\")''').encode('utf-8')
        try:
            errors = self.target.query(_query)
        except Exception as err:
            logEvents.error('Host {} unable to retrieve events  ({})'.format(self.getHostName(), err))
            return False
        else:
            logEvents.info('Host {} events received [{}]'.format(self.getHostName(), len(errors)))
        finally:
            print '   ... Done'
        if len(errors) == 0: return False
        tsearch = re.compile(r'^(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})(?P<hour>\d{2})(?P<minute>\d{2})(?P<second>\d{2})')
        kwsearch = re.compile(r'\b((?<![\.])[A-Z_]{4,}\b|(?<=name: )[A-Za-z_]+?\.\w{1,3}\b|(?<! stamp: |\w time: |offset: )0x[a-z0-9]{8}\b)')
        logEvents.info('Processing events')
        for error in errors:
            code, log, timegenerated, source, record = error.eventcode, str(error.logfile), \
                                                       str(error.timegenerated), str(error.sourcename), \
                                                       error.recordnumber
            logEvents.debug('Processing event: {}, {}, {}'.format(code, source, record))
            try:
                message = (error.message).decode('utf-8')
            except Exception as err:
                message = str(error.message)
                logEvents.warning('Unable to decode message: {}, {}, {}  ({})'.format(code, source, record, err))
            key = log + ' :: ' + str(code) + ' :: ' + source
            t = re.search(tsearch, timegenerated)
            time = '{0}/{1}/{2} {3}:{4}:{5}'.format(t.group('year'), t.group('month'), t.group('day'),
                                                    t.group('hour'), t.group('minute'), t.group('second'))
            if message:
                kwords = ' '.join(list(set(re.findall(kwsearch, message))))
                kwords = kwords + ' ' + message[:30] + ' ' + str(code) + ' ' + source
            else:
                kwords = ''
            self.e_list.append([log, record, code, source, message, time, kwords])
            if key in _cache:
                _cache[key][0] += 1
                if time < _cache[key][1]: _cache[key][1] = time
                elif time > _cache[key][2]: _cache[key][2] = time
            else:
                _cache[key] = [1, time, time, message, kwords]
        for item in _cache.items():
            start = item[0].split(' :: ')
            start.extend([item[1][3], item[1][1], item[1][2], item[1][0], item[1][4]])
            self.e_summary.append(start)
        self.e_summary.sort(key=itemgetter(6), reverse=True)
        self.e_list.sort(key=itemgetter(4), reverse=True)
        self.e_summary.insert(0, self.e_sum_header)
        self.e_list.insert(0, self.e_header)
        logEvents.info('Host {} event processing complete'.format(self.getHostName()))
        return True


def get_hosts():
    """ Build a list of hosts. """
    host_class = GetNet()
    if config.HostFile:
        hosts = host_class.hostfile(config.HostFile)
    else:
        hosts = host_class.options()
        logDefault.info('Hosts found: {}'.format(len(hosts)))
    print ''
    print 'Hosts ({0}):\n\n'.format(len(hosts)), '\n'.join([str(host) for host in hosts])
    print ''
    return hosts


def host_connect(host, shortHost):
    """ Connect to host.  Return None if it fails. """
    print 'Connect to host {}'.format(shortHost)
    try:
        target = wmiData(host, shortHost)
    except:
        print ' *** Connection Failed ***'
        logConn.error('Host {} connection failed.'.format(shortHost))
        target = None
    else:
        logConn.info('Host {} connected.'.format(shortHost))
    return target


def scan_summary(target, host, shortHost):
    """ Retrieve data to be written out. """
    summary = [host, Var.scandate]
    opts = [target.Bios, target.Processor, target.CompSys,
            target.Video, target.OS]
    for opt in opts:
        try:
            summary.extend(opt())
        except Exception as err:
            logDefault.error('Host {} could not load summary  ({})'.format(shortHost, err))
    disk = target.FreeDisk()
    print '   ... Done'
    if disk:
        Var.disk_alert.extend(disk)
        logDefault.info('Host {} disk space alert'.format(shortHost))
    if len(summary) < 21:
        logDefault.info('Some attributes are missing; filling in space.')
        summary.extend([''] * (21 - len(summary)))
    elif len(summary) > 21:
        logDefault.info('Too many attributes found; truncating.')
        summary = summary[:21]
    logDefault.info('Host {} summary loaded'.format(shortHost))
    return summary


def host_summary(target, host, shortHost, hostDir):
    csvout = []
    csvout.append([host, Var.scandate])
    csvout.extend(Var.csv_space)
    opts = [target.Processes, target.Services, target.Startup, target.Printers]
    for opt in opts:
        category, header, data = opt()
        if not header:
            logDefault.warning('Host {} no data found: {}'.format(shortHost, category[0]))
            category = header = data = ''
        else:
            logDefault.info('Host {} data found: {}'.format(shortHost, category[0]))
            print '   ... Done'
        csvout.append(category)
        csvout.append(header)
        csvout.extend(sorted(data))
        csvout.extend(Var.csv_space)
    try:
        Var.fm.CSVComplete('host_summary.csv', csvout, hostDir)
        Var.fm.XLSWrite('{}'.format(shortHost), csvout)
    except Exception as err:
        logDefault.error('Host {} could not write summary file ({})'.format(shortHost, err))
        return False
    else:
        logDefault.info('Host {} summary file written'.format(shortHost))
        return True


def host_events(target, host, shortHost, hostDir):
    """ Process event logs. """
    if not target.EventLog():
        logEvents.info('Host {} no event records to report'.format(shortHost))
        e_summary = e_list = [['No Records']]
    else:
        e_summary, e_list = target.e_summary, target.e_list
        Var.database.addToEvents(host, e_list)
    Var.event_count.append((host, (len(e_list) - 1)))
    try:
        Var.fm.CSVComplete('event_summary.csv', e_summary, hostDir)
        Var.fm.CSVComplete('event_list.csv', e_list, hostDir)
        Var.fm.XLSWrite('{} Events'.format(shortHost), e_summary)
        Var.fm.XLSWrite('{} All Events'.format(shortHost), e_list)
    except Exception as err:
        logDefault.error('Host {} could not write event log ({})'.format(shortHost, err))
        raise
        return False
    else:
        logDefault.debug('Host {} event log written.'.format(shortHost))
        return True


def makeHostDir(host):
    mDate = '{0} - {1}'.format(Var.today[0], Var.today[1])
    hostDir = os.path.join(Dirs.allHostsDir, host, mDate, Var.fdate)
    try:
        os.makedirs(hostDir)
    except WindowsError:
        logDefault.info('Host directory already exists.')
    return hostDir


def build_summary():
    Var.sum_data.insert(0, Var.sum_header)
    Var.sum_data.insert(0, ['Summary'])
    if Var.bad_hosts:
        print ''
        print 'Could not connect to the following hosts:\n'
        print '\n'.join(Var.bad_hosts)
        Var.sum_data.extend(Var.csv_space)
        Var.sum_data.append(['The following machines were unreachable'])
        Var.sum_data.extend([[e] for e in Var.bad_hosts])
    if Var.disk_alert:
        print ''
        print 'The following machines are low on disk space:\n'
        print '\n'.join(['Host: {0:12}  ::  {1}  -->  {2}%'.format(name.split('.')[0], drive, space)
                        for name, drive, space in Var.disk_alert])
        Var.sum_data.extend(Var.csv_space)
        Var.sum_data.append(['Free Disk'])
        Var.sum_data.append(['Host', 'Drive', '% Free Space'])
        Var.sum_data.extend(Var.disk_alert)
    if config.EventHistory != -1:
        print ''
        print 'Event count:\n'
        print '\n'.join(['Host: {0:12}  ::  Events: {1}'.format(makeShort(name), str(count))
                        for name, count in Var.event_count])
        Var.sum_data.extend(Var.csv_space)
        Var.sum_data.append(['Event Count'])
        Var.sum_data.append(['Host', 'Count'])
        Var.sum_data.extend(Var.event_count)


def write_summary():
    now = datetime.datetime.now()
    date = '%d - %0*d' % (now.year, 2, now.month)
    scanSummaryDir = os.path.join(Dirs.summaryDir, date)
    try:
        os.makedirs(scanSummaryDir)
    except WindowsError:
        logDefault.info('Directories already exist; not creating more.')
    Var.fm.XLSsWrite('Scan Summary', Var.sum_data)
    Var.fm.XLSSave(os.path.join(scanSummaryDir, 'scan_summary - {}.xls'.format(Var.fdate)))


def writeDB():
    logDefault.info('Writing to database')
    Var.database.Connect()
    Var.database.AddTable('summary', Var.sum_table)
    Var.database.AddTable('events', Var.event_table)
    Var.database.WriteSummary()
    Var.database.WriteEvents()
    Var.database.Commit()


def makeShort(host):
    ipPattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
    if re.search(ipPattern, host):
        return host
    else:
        return host.split('.')[0].upper()


def scan_manager():
    hosts = get_hosts()
    Var.fm.XLSAddSheet('Scan Summary')
    print ''
    try:
        raw_input('Press Enter to continue or control-c to break out.')
    except KeyboardInterrupt:
        print 'Good-bye!'
        sys.exit()
    print ''
    for host in hosts:
        shortHost = makeShort(host)
        logDefault.info('***')
        logDefault.info('===========  {} begin ==========='.format(shortHost))
        logDefault.info('***')
        print ''
        target = host_connect(host, shortHost)
        if target is None:
            Var.bad_hosts.append(host)
            logDefault.info('***')
            logDefault.info('===========  {} end ==========='.format(shortHost))
            logDefault.info('***')
            continue
        hostDir = makeHostDir(host)
        try:
            summary = scan_summary(target, host, shortHost)
        except:
            logDefault.error('Host {} problem detected, skipping host'.format(shortHost))
            continue
        Var.sum_data.append(summary)
        Var.database.addToSummary(tuple(Var.sum_data[-1]))
        if not host_summary(target, host, shortHost, hostDir):
            logDefault.error('Host {} data not successfully written.'.format(shortHost))
        if config.EventHistory != -1:
            logEvents.info('Gathering events for host {}'.format(shortHost))
            if not host_events(target, host, shortHost, hostDir):
                logDefault.error('Host {} events not successfully written.'.format(shortHost))
        logDefault.info('***')
        logDefault.info('===========  {} end ==========='.format(shortHost))
        logDefault.info('***')
    build_summary()
    write_summary()
    writeDB()
    print ''
    raw_input('Press ENTER to exit.')
    Popen('explorer {}'.format(Dirs.scanDir))
    logDefault.info('Program complete')

def makeDirs():
    for folder in [Dirs.scanDir, Dirs.allHostsDir, Dirs.summaryDir]:
        if not os.path.exists(folder):
            os.mkdir(folder)

def main():
    makeDirs()
    scan_manager()



if __name__ == '__main__':
    main()
